package org.red5.io.mp3.impl;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.mina.common.ByteBuffer;
import org.red5.io.IKeyFrameMetaCache;
import org.red5.io.IStreamableFile;
import org.red5.io.ITag;
import org.red5.io.ITagReader;
import org.red5.io.IoConstants;
import org.red5.io.amf.Output;
import org.red5.io.flv.IKeyFrameDataAnalyzer;
import org.red5.io.flv.impl.Tag;
import org.red5.io.object.Serializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class YAMP3Reader implements ITagReader, IKeyFrameDataAnalyzer {
    /**
     * Logger
     */
	protected static Logger log = LoggerFactory.getLogger(YAMP3Reader.class);

    /**
     * File
     */
    private File file;
    
    private RandomAccessFile raf;
        
    private long fileSize;
    /**
     * Last read tag object
     */
	private ITag tag;
    /**
     * Previous tag size
     */
	private int prevSize;
    /**
     * Current time
     */
	private double currentTime;
    /**
     * Frame metadata
     */
	private KeyFrameMeta frameMeta;
    /**
     * Positions and time map
     */
	private HashMap<Integer, Double> posTimeMap;

	private int dataRate;
    /**
     * Whether first frame is read
     */
	private boolean firstFrame;
    /**
     * File metadata
     */
	private ITag fileMeta;
    /**
     * File duration
     */
	private long duration;
    /**
     * Frame cache
     */
	static private IKeyFrameMetaCache frameCache;

	YAMP3Reader() {
		// Only used by the bean startup code to initialize the frame cache
	}
	
    /**
     * Creates reader from file input stream
     * @param stream          File input stream source
     */
    public YAMP3Reader(File file) throws IOException {
    	try {
        	this.file = file;
        	this.raf = new RandomAccessFile(file, "r");
        	this.fileSize = raf.length();
        	// Analyze keyframes data
            analyzeKeyFrames();
    		firstFrame = true;
    		
            // Process ID3v2 header if present
    		processID3v2Header();
    		
            // Create file metadata object
            fileMeta = createFileMeta();
    
            // MP3 header is length of 32 bits, that is, 4 bytes
            // Read further if there's still data
            if (bytesRemaining() > 4) {
                // Look to next frame
                searchNextFrame();
                // Set position
                long pos = raf.getFilePointer();
                // Read header...
                // Data in MP3 file goes header-data-header-data...header-data
                MP3Header header = readHeader();
                // Set position
                raf.seek(pos);
                // Check header
                if (header != null) {
    				checkValidHeader(header);
    			} else {
    				throw new RuntimeException("No initial header found.");
    			}
    		}
    	} catch (IOException e) {
    		e.printStackTrace();
    	}
	}

    /**
     * A MP3 stream never has video.
     * 
     * @return always returns <code>false</code>
     */
    public boolean hasVideo() {
    	return false;
    }
    
    public void setFrameCache(IKeyFrameMetaCache frameCache) {
    	YAMP3Reader.frameCache = frameCache;
    }
    
	/**
	 * Check if the file can be played back with Flash. Supported sample rates are
     * 44KHz, 22KHz, 11KHz and 5.5KHz
     * 
	 * @param header       Header to check
	 */
	private void checkValidHeader(MP3Header header) {
		switch (header.getSampleRate()) {
			case 44100:
			case 22050:
			case 11025:
			case 5513:
				// Supported sample rate
				break;

			default:
				throw new RuntimeException("Unsupported sample rate: "
						+ header.getSampleRate());
		}
	}

    /**
     * Creates file metadata object
     * @return         Tag
     */
    private ITag createFileMeta() {
		// Create tag for onMetaData event
		ByteBuffer buf = ByteBuffer.allocate(1024);
		buf.setAutoExpand(true);
		Output out = new Output(buf);
		out.writeString("onMetaData");
		Map<Object, Object> props = new HashMap<Object, Object>();
		props.put("duration", frameMeta.timestamps[frameMeta.timestamps.length - 1] / 1000.0);
		props.put("audiocodecid", IoConstants.FLAG_FORMAT_MP3);
		if (dataRate > 0) {
			props.put("audiodatarate", dataRate);
		}
		props.put("canSeekToEnd", true);
		out.writeMap(props, new Serializer());
		buf.flip();

		ITag result = new Tag(IoConstants.TYPE_METADATA, 0, buf.limit(), null,
				prevSize);
		result.setBody(buf);
		return result;
	}

	/** Search for next frame sync word. Sync word identifies valid frame. */
	public void searchNextFrame() throws IOException {
		while (bytesRemaining() > 1) {
			int ch = raf.readUnsignedByte();
			if (ch != 0xff) {
				continue;
			}

			if ((raf.readUnsignedByte() & 0xe0) == 0xe0) {
				// Found it
				raf.seek(raf.getFilePointer() - 2);
				return;
			}
		}
	}

	/** {@inheritDoc} */
    public IStreamableFile getFile() {
		// TODO Auto-generated method stub
		return null;
	}

	/** {@inheritDoc} */
    public int getOffset() {
		// TODO Auto-generated method stub
		return 0;
	}

	/** {@inheritDoc} */
    public long getBytesRead() {
    	try {
    		return raf.getFilePointer();
    	} catch (IOException e) {
    		return 0;
    	}
	}

	/** {@inheritDoc} */
    public long getDuration() {
		return duration;
	}

	/** {@inheritDoc} */
    public boolean hasMoreTags() {
    	try {
    		MP3Header header = null;
    		while (header == null && bytesRemaining() > 4) {
    			try {
    				header = new MP3Header(raf.readInt());
    			} catch (IOException e) {
    				log.error("MP3Reader :: hasMoreTags ::>\n", e);
    				break;
    			} catch (Exception e) {
    				searchNextFrame();
    			}
    		}
    
    		if (header == null) {
    			return false;
    		}
    
    		if (header.frameSize() == 0) {
    			// TODO find better solution how to deal with broken files...
    			// See APPSERVER-62 for details
    			return false;
    		}
    		
    		if (raf.getFilePointer() + header.frameSize() - 4 > fileSize) {
    			// Last frame is incomplete
    			raf.seek(fileSize);
    			return false;
    		}
    
    		raf.seek(raf.getFilePointer() - 4);
    		return true;
    	} catch (IOException e) {
    		e.printStackTrace();
    		return false;
    	}
	}

	private MP3Header readHeader() throws IOException {
		MP3Header header = null;
		while (header == null && bytesRemaining() > 4) {
			try {
				header = new MP3Header(raf.readInt());
			} catch (IOException e) {
				log.error("MP3Reader :: readTag ::>\n", e);
				break;
			} catch (Exception e) {
				searchNextFrame();
			}
		}
		return header;
	}

	/** {@inheritDoc} */
    public synchronized ITag readTag() {
    	try {
    		if (firstFrame) {
    			// Return file metadata as first tag.
    			firstFrame = false;
    			return fileMeta;
    		}
    
    		MP3Header header = readHeader();
    		if (header == null) {
    			return null;
    		}
    
    		int frameSize = header.frameSize();
    		if (frameSize == 0) {
    			// TODO find better solution how to deal with broken files...
    			// See APPSERVER-62 for details
    			return null;
    		}
    		
    		if (raf.getFilePointer() + frameSize - 4 > fileSize) {
    			// Last frame is incomplete
    			raf.seek(fileSize);
    			return null;
    		}
    
    		tag = new Tag(IoConstants.TYPE_AUDIO, (int) currentTime, frameSize + 1,
    				null, prevSize);
    		prevSize = frameSize + 1;
    		currentTime += header.frameDuration();
    		ByteBuffer body = ByteBuffer.allocate(tag.getBodySize());
    		byte tagType = (IoConstants.FLAG_FORMAT_MP3 << 4)
    				| (IoConstants.FLAG_SIZE_16_BIT << 1);
    		switch (header.getSampleRate()) {
    			case 44100:
    				tagType |= IoConstants.FLAG_RATE_44_KHZ << 2;
    				break;
    			case 22050:
    				tagType |= IoConstants.FLAG_RATE_22_KHZ << 2;
    				break;
    			case 11025:
    				tagType |= IoConstants.FLAG_RATE_11_KHZ << 2;
    				break;
    			default:
    				tagType |= IoConstants.FLAG_RATE_5_5_KHZ << 2;
    		}
    		tagType |= (header.isStereo() ? IoConstants.FLAG_TYPE_STEREO
    				: IoConstants.FLAG_TYPE_MONO);
    		body.put(tagType);
    		body.putInt(header.getData());
    		byte[] buf = new byte[frameSize - 4]; // TODO use memory more economically
    		raf.read(buf);
    		body.put(buf);
    		body.flip();
    
    		tag.setBody(body);
    
    		return tag;
    	} catch (IOException e) {
    		e.printStackTrace();
    		return null;
    	}
	}

	/** {@inheritDoc} */
    public void close() {
		if (posTimeMap != null) {
			posTimeMap.clear();
		}
		try {
			raf.close();
		} catch (IOException e) {
			log.error("MP3Reader :: close ::>\n", e);
		}
	}

	/** {@inheritDoc} */
    public void decodeHeader() {
	}

	/** {@inheritDoc} */
    public void position(long pos) {
    	try {
        	if (pos == Long.MAX_VALUE) {
        		// Seek at EOF
        		raf.seek(fileSize);
        		currentTime = duration;
        		return;
        	}
    		raf.seek(pos);
    		// Advance to next frame
    		searchNextFrame();
    		// Make sure we can resolve file positions to timestamps
    		analyzeKeyFrames();
    		Double time = posTimeMap.get((int)raf.getFilePointer());
    		if (time != null) {
    			currentTime = time;
    		} else {
    			// Unknown frame position - this should never happen
    			currentTime = 0;
    		}
    	} catch (IOException e) {
    		e.printStackTrace();
    	}
	}

	/** {@inheritDoc} */
    public synchronized KeyFrameMeta analyzeKeyFrames() {
		if (frameMeta != null) {
			return frameMeta;
		}

		// check for cached frame informations
		if (frameCache != null) {
			frameMeta = frameCache.loadKeyFrameMeta(file);
			if (frameMeta != null && frameMeta.duration > 0) {
				// Frame data loaded, create other mappings
				duration = frameMeta.duration;
				frameMeta.audioOnly = true;
				posTimeMap = new HashMap<Integer, Double>();
				for (int i=0; i<frameMeta.positions.length; i++) {
					posTimeMap.put((int) frameMeta.positions[i], (double) frameMeta.timestamps[i]);
				}
				return frameMeta;
			}
		}
		try {

    		List<Integer> positionList = new ArrayList<Integer>();
    		List<Double> timestampList = new ArrayList<Double>();
    		dataRate = 0;
    		long rate = 0;
    		int count = 0;
    		long origPos = raf.getFilePointer();
    		double time = 0;
    		raf.seek(0);
    		processID3v2Header();
    		searchNextFrame();
    		while (this.hasMoreTags()) {
    			MP3Header header = readHeader();
    			if (header == null) {
    				// No more tags
    				break;
    			}
    
    			if (header.frameSize() == 0) {
    				// TODO find better solution how to deal with broken files...
    				// See APPSERVER-62 for details
    				break;
    			}
    			
    			long pos = raf.getFilePointer() - 4;
    			if (pos + header.frameSize() > fileSize) {
    				// Last frame is incomplete
    				break;
    			}
    
    			positionList.add((int)pos);
    			timestampList.add(time);
    			rate += header.getBitRate() / 1000;
    			time += header.frameDuration();
    			raf.seek(pos + header.frameSize());
    			count++;
    		}
    		// restore the pos
    		raf.seek(origPos);
    
    		duration = (long) time;
    		dataRate = (int) (rate / count);
    		posTimeMap = new HashMap<Integer, Double>();
    		frameMeta = new KeyFrameMeta();
    		frameMeta.duration = duration;
    		frameMeta.positions = new long[positionList.size()];
    		frameMeta.timestamps = new int[timestampList.size()];
    		frameMeta.audioOnly = true;
    		for (int i = 0; i < frameMeta.positions.length; i++) {
    			frameMeta.positions[i] = positionList.get(i);
    			frameMeta.timestamps[i] = timestampList.get(i).intValue();
    			posTimeMap.put(positionList.get(i), timestampList.get(i));
    		}
    		if (frameCache != null)
    			frameCache.saveKeyFrameMeta(file, frameMeta);
    		
    		return frameMeta;
		} catch (IOException e) {
			e.printStackTrace();
			frameMeta = null;
			return null;
		}
	}

    private void processID3v2Header() throws IOException {
    	if (bytesRemaining() <= 10)
    		// We need at least 10 bytes ID3v2 header + data
    		return;
    	
    	long origPos = raf.getFilePointer();
    	byte a, b, c;
    	a = (byte) raf.readUnsignedByte();
    	b = (byte) raf.readUnsignedByte();
    	c = (byte) raf.readUnsignedByte();
    	if (a != 'I' || b != 'D' || c != '3') {
    		// No ID3v2 header
    		raf.seek(origPos);
    		return;
    	}
    	
    	// Skip version and flags
    	raf.skipBytes(3);
    	int size = (raf.readUnsignedByte() & 0x7f) << 21 |
    		(raf.readUnsignedByte() & 0x7f) << 14 |
    		(raf.readUnsignedByte() & 0x7f) << 7 |
    		(raf.readUnsignedByte() & 0x7f);
    	// Skip ID3v2 header for now
    	raf.skipBytes(size);
    }
    
    private long bytesRemaining() throws IOException {
    	return this.fileSize - this.raf.getFilePointer();
    }
}
